Pelajari cara menguji aplikasi FastAPI Anda secara efektif menggunakan TestClient. Mencakup praktik terbaik, teknik lanjutan, dan contoh dunia nyata untuk API yang kuat dan andal.
Menguasai Pengujian FastAPI: Panduan Komprehensif untuk TestClient
FastAPI telah muncul sebagai kerangka kerja terkemuka untuk membangun API berperforma tinggi dengan Python. Kecepatannya, kemudahan penggunaannya, dan validasi data otomatis menjadikannya favorit di kalangan pengembang di seluruh dunia. Namun, API yang dibuat dengan baik hanya sebaik pengujiannya. Pengujian menyeluruh memastikan bahwa API Anda berfungsi seperti yang diharapkan, tetap stabil di bawah tekanan, dan dapat dengan percaya diri diterapkan ke produksi. Panduan komprehensif ini berfokus pada penggunaan TestClient FastAPI untuk menguji titik akhir API Anda secara efektif.
Mengapa Pengujian Penting untuk Aplikasi FastAPI?
Pengujian adalah langkah penting dalam siklus hidup pengembangan perangkat lunak. Ini membantu Anda:
- Mengidentifikasi bug lebih awal: Menangkap kesalahan sebelum mencapai produksi, menghemat waktu dan sumber daya.
- Memastikan kualitas kode: Mempromosikan kode yang terstruktur dengan baik dan mudah dipelihara.
- Mencegah regresi: Menjamin bahwa perubahan baru tidak merusak fungsionalitas yang ada.
- Meningkatkan keandalan API: Membangun kepercayaan pada stabilitas dan kinerja API.
- Memfasilitasi kolaborasi: Menyediakan dokumentasi yang jelas tentang perilaku yang diharapkan untuk pengembang lain.
Memperkenalkan TestClient FastAPI
FastAPI menyediakan TestClient bawaan yang menyederhanakan proses pengujian titik akhir API Anda. TestClient bertindak sebagai klien ringan yang dapat mengirim permintaan ke API Anda tanpa memulai server yang lengkap. Hal ini membuat pengujian menjadi jauh lebih cepat dan lebih nyaman.
Fitur Utama TestClient:
- Mensimulasikan permintaan HTTP: Memungkinkan Anda mengirim permintaan GET, POST, PUT, DELETE, dan permintaan HTTP lainnya ke API Anda.
- Menangani serialisasi data: Secara otomatis men-serialisasi data permintaan (misalnya, payload JSON) dan men-de-serialisasi data respons.
- Menyediakan metode pernyataan: Menawarkan metode yang nyaman untuk memverifikasi kode status, header, dan konten respons.
- Mendukung pengujian asinkron: Bekerja tanpa hambatan dengan sifat asinkron FastAPI.
- Berintegrasi dengan kerangka kerja pengujian: Mudah berintegrasi dengan kerangka kerja pengujian Python populer seperti pytest dan unittest.
Menyiapkan Lingkungan Pengujian Anda
Sebelum Anda mulai menguji, Anda perlu menyiapkan lingkungan pengujian Anda. Ini biasanya melibatkan penginstalan dependensi yang diperlukan dan konfigurasi kerangka kerja pengujian Anda.
Instalasi
Pertama, pastikan Anda telah menginstal FastAPI dan pytest. Anda dapat menginstalnya menggunakan pip:
pip install fastapi pytest httpx
httpx adalah klien HTTP yang digunakan FastAPI di balik layar. Sementara TestClient adalah bagian dari FastAPI, memiliki httpx yang terinstal juga memastikan pengujian yang lancar. Beberapa tutorial juga menyebutkan requests, namun, httpx lebih selaras dengan sifat asinkron FastAPI.
Contoh Aplikasi FastAPI
Mari buat aplikasi FastAPI sederhana yang dapat kita gunakan untuk pengujian:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
@app.post("/items/")
async def create_item(item: Item):
return item
Simpan kode ini sebagai main.py. Aplikasi ini mendefinisikan tiga titik akhir:
/: Titik akhir GET sederhana yang mengembalikan pesan "Hello World"./items/{item_id}: Titik akhir GET yang mengembalikan item berdasarkan ID-nya./items/: Titik akhir POST yang membuat item baru.
Menulis Pengujian Pertama Anda
Sekarang setelah Anda memiliki aplikasi FastAPI, Anda dapat mulai menulis pengujian menggunakan TestClient. Buat file baru bernama test_main.py di direktori yang sama dengan main.py.
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
Dalam pengujian ini:
- Kami mengimpor
TestClientdan instanceappFastAPI. - Kami membuat instance
TestClient, dengan meneruskanapp. - Kami mendefinisikan fungsi pengujian
test_read_root. - Di dalam fungsi pengujian, kami menggunakan
client.get("/")untuk mengirim permintaan GET ke titik akhir root. - Kami menegaskan bahwa kode status respons adalah 200 (OK).
- Kami menegaskan bahwa respons JSON sama dengan
{"message": "Hello World"}.
Menjalankan Pengujian Anda dengan pytest
Untuk menjalankan pengujian Anda, cukup buka terminal di direktori yang berisi file test_main.py Anda dan jalankan perintah berikut:
pytest
pytest akan secara otomatis menemukan dan menjalankan semua pengujian di proyek Anda. Anda akan melihat output yang mirip dengan ini:
============================= test session starts ==============================
platform darwin -- Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /path/to/your/project
collected 1 item
test_main.py .
============================== 1 passed in 0.01s ===============================
Menguji Metode HTTP yang Berbeda
TestClient mendukung semua metode HTTP standar, termasuk GET, POST, PUT, DELETE, dan PATCH. Mari kita lihat cara menguji masing-masing metode ini.
Menguji Permintaan GET
Kita sudah melihat contoh pengujian permintaan GET di bagian sebelumnya. Berikut adalah contoh lain, menguji titik akhir /items/{item_id}:
def test_read_item():
response = client.get("/items/1?q=test")
assert response.status_code == 200
assert response.json() == {"item_id": 1, "q": "test"}
Pengujian ini mengirimkan permintaan GET ke /items/1 dengan parameter kueri q=test. Kemudian menegaskan bahwa kode status respons adalah 200 dan bahwa respons JSON berisi data yang diharapkan.
Menguji Permintaan POST
Untuk menguji permintaan POST, Anda perlu mengirim data dalam badan permintaan. TestClient secara otomatis men-serialisasi data ke JSON.
def test_create_item():
item_data = {"name": "Example Item", "description": "A test item", "price": 9.99, "tax": 1.00}
response = client.post("/items/", json=item_data)
assert response.status_code == 200
assert response.json() == item_data
Dalam pengujian ini:
- Kami membuat kamus
item_datayang berisi data untuk item baru. - Kami menggunakan
client.post("/items/", json=item_data)untuk mengirim permintaan POST ke titik akhir/items/, dengan meneruskanitem_datasebagai payload JSON. - Kami menegaskan bahwa kode status respons adalah 200 dan bahwa respons JSON cocok dengan
item_data.
Menguji Permintaan PUT, DELETE, dan PATCH
Menguji permintaan PUT, DELETE, dan PATCH mirip dengan menguji permintaan POST. Anda cukup menggunakan metode yang sesuai pada TestClient:
def test_update_item():
item_data = {"name": "Updated Item", "description": "An updated test item", "price": 19.99, "tax": 2.00}
response = client.put("/items/1", json=item_data)
assert response.status_code == 200
# Tambahkan pernyataan untuk respons yang diharapkan
def test_delete_item():
response = client.delete("/items/1")
assert response.status_code == 200
# Tambahkan pernyataan untuk respons yang diharapkan
def test_patch_item():
item_data = {"price": 29.99}
response = client.patch("/items/1", json=item_data)
assert response.status_code == 200
# Tambahkan pernyataan untuk respons yang diharapkan
Ingatlah untuk menambahkan pernyataan untuk memverifikasi bahwa respons sesuai dengan yang diharapkan.
Teknik Pengujian Tingkat Lanjut
TestClient menawarkan beberapa fitur tingkat lanjut yang dapat membantu Anda menulis pengujian yang lebih komprehensif dan efektif.
Pengujian dengan Dependensi
Sistem injeksi dependensi FastAPI memungkinkan Anda untuk dengan mudah menyuntikkan dependensi ke dalam titik akhir API Anda. Saat menguji, Anda mungkin ingin mengganti dependensi ini untuk memberikan implementasi mock atau khusus pengujian.
Misalnya, misalkan aplikasi Anda bergantung pada koneksi database. Anda dapat mengganti dependensi database dalam pengujian Anda untuk menggunakan database dalam memori:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker, declarative_base, Session
# Konfigurasi Database
DATABASE_URL = "sqlite:///./test.db" # Database dalam memori untuk pengujian
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Definisikan Model Pengguna
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String)
Base.metadata.create_all(bind=engine)
# Aplikasi FastAPI
app = FastAPI()
# Dependensi untuk mendapatkan sesi database
def get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
# Titik akhir untuk membuat pengguna
@app.post("/users/")
async def create_user(username: str, password: str, db: Session = Depends(get_db)):
db_user = User(username=username, password=password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
from fastapi.testclient import TestClient
from .main import app, get_db, Base, engine, TestingSessionLocal
client = TestClient(app)
# Ganti dependensi database untuk pengujian
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
# Pertama, pastikan tabel dibuat, yang mungkin tidak terjadi secara default
Base.metadata.create_all(bind=engine) # penting: buat tabel di db pengujian
response = client.post("/users/", params={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert response.json()["username"] == "testuser"
# Bersihkan penggantian setelah pengujian jika diperlukan
app.dependency_overrides = {}
Contoh ini mengganti dependensi get_db dengan fungsi khusus pengujian yang mengembalikan sesi ke database SQLite dalam memori. Penting: Pembuatan metadata harus dipanggil secara eksplisit agar db pengujian berfungsi dengan benar. Gagal membuat tabel akan menyebabkan kesalahan terkait tabel yang hilang.
Menguji Kode Asinkron
FastAPI dibangun untuk menjadi asinkron, jadi Anda akan sering perlu menguji kode asinkron. TestClient mendukung pengujian asinkron tanpa hambatan.
Untuk menguji titik akhir asinkron, cukup definisikan fungsi pengujian Anda sebagai async:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(0.1) # Mensimulasikan beberapa operasi asinkron
return {"message": "Async Hello"}
import pytest
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
@pytest.mark.asyncio # Diperlukan agar kompatibel dengan pytest-asyncio
async def test_async_endpoint():
response = client.get("/async")
assert response.status_code == 200
assert response.json() == {"message": "Async Hello"}
Catatan: Anda perlu menginstal pytest-asyncio untuk menggunakan @pytest.mark.asyncio: pip install pytest-asyncio. Anda juga perlu memastikan asyncio.get_event_loop() dikonfigurasi jika menggunakan versi pytest yang lebih lama. Jika menggunakan pytest versi 8 atau yang lebih baru, ini mungkin tidak diperlukan.
Menguji Unggahan File
FastAPI memudahkan penanganan unggahan file. Untuk menguji unggahan file, Anda dapat menggunakan parameter files dari metode permintaan TestClient.
from fastapi import FastAPI, File, UploadFile
from typing import List
app = FastAPI()
@app.post("/files/")
async def create_files(files: List[bytes] = File()):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile]):
return {"filenames": [file.filename for file in files]}
from fastapi.testclient import TestClient
from .main import app
import io
client = TestClient(app)
def test_create_files():
file_content = b"Test file content"
files = [('files', ('test.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/files/", files=files)
assert response.status_code == 200
assert response.json() == {"file_sizes": [len(file_content)]}
def test_create_upload_files():
file_content = b"Test upload file content"
files = [('files', ('test_upload.txt', io.BytesIO(file_content), 'text/plain'))]
response = client.post("/uploadfiles/", files=files)
assert response.status_code == 200
assert response.json() == {"filenames": ["test_upload.txt"]}
Dalam pengujian ini, kami membuat file dummy menggunakan io.BytesIO dan meneruskannya ke parameter files. Parameter files menerima daftar tupel, di mana setiap tupel berisi nama bidang, nama file, dan konten file. Jenis konten penting untuk penanganan yang akurat oleh server.
Menguji Penanganan Kesalahan
Penting untuk menguji bagaimana API Anda menangani kesalahan. Anda dapat menggunakan TestClient untuk mengirim permintaan yang tidak valid dan memverifikasi bahwa API mengembalikan respons kesalahan yang benar.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id > 100:
raise HTTPException(status_code=400, detail="Item ID too large")
return {"item_id": item_id}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_item_error():
response = client.get("/items/101")
assert response.status_code == 400
assert response.json() == {"detail": "Item ID too large"}
Pengujian ini mengirimkan permintaan GET ke /items/101, yang memunculkan HTTPException dengan kode status 400. Pengujian menegaskan bahwa kode status respons adalah 400 dan bahwa respons JSON berisi pesan kesalahan yang diharapkan.
Menguji Fitur Keamanan
Jika API Anda menggunakan otentikasi atau otorisasi, Anda juga perlu menguji fitur keamanan ini. TestClient memungkinkan Anda untuk mengatur header dan cookie untuk mensimulasikan permintaan yang diautentikasi.
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
app = FastAPI()
# Keamanan
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Simulasikan otentikasi
if form_data.username != "testuser" or form_data.password != "password123":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
return {"access_token": "fake_token", "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
return {"message": "Protected data"}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_login():
response = client.post("/token", data={"username": "testuser", "password": "password123"})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route():
# Pertama, dapatkan token
token_response = client.post("/token", data={"username": "testuser", "password": "password123"})
token = token_response.json()["access_token"]
# Kemudian, gunakan token untuk mengakses rute yang dilindungi
response = client.get("/protected", headers={"Authorization": f"Bearer {token}"}) # format yang dikoreksi.
assert response.status_code == 200
assert response.json() == {"message": "Protected data"}
Dalam contoh ini, kami menguji titik akhir login dan kemudian menggunakan token yang diperoleh untuk mengakses rute yang dilindungi. Parameter headers dari metode permintaan TestClient memungkinkan Anda untuk mengatur header khusus, termasuk header Authorization untuk token bearer.
Praktik Terbaik untuk Pengujian FastAPI
Berikut adalah beberapa praktik terbaik yang harus diikuti saat menguji aplikasi FastAPI Anda:
- Tulis pengujian yang komprehensif: Usahakan untuk mendapatkan cakupan pengujian yang tinggi untuk memastikan bahwa semua bagian API Anda diuji secara menyeluruh.
- Gunakan nama pengujian yang deskriptif: Pastikan nama pengujian Anda dengan jelas menunjukkan apa yang diverifikasi oleh pengujian.
- Ikuti pola Arrange-Act-Assert: Atur pengujian Anda menjadi tiga fase yang berbeda: Arrange (siapkan data pengujian), Act (lakukan tindakan yang sedang diuji), dan Assert (verifikasi hasilnya).
- Gunakan objek mock: Mock dependensi eksternal untuk mengisolasi pengujian Anda dan menghindari ketergantungan pada sistem eksternal.
- Uji kasus tepi: Uji API Anda dengan input yang tidak valid atau tidak terduga untuk memastikan bahwa API menangani kesalahan dengan baik.
- Jalankan pengujian secara berkala: Integrasikan pengujian ke dalam alur kerja pengembangan Anda untuk menangkap bug lebih awal dan sering.
- Berintegrasi dengan CI/CD: Otomatiskan pengujian Anda di pipeline CI/CD Anda untuk memastikan bahwa semua perubahan kode diuji secara menyeluruh sebelum diterapkan ke produksi. Alat seperti Jenkins, GitLab CI, GitHub Actions, atau CircleCI dapat digunakan untuk mencapai ini.
Contoh: Pengujian Internasionalisasi (i18n)
Saat mengembangkan API untuk audiens global, internasionalisasi (i18n) sangat penting. Pengujian i18n melibatkan verifikasi bahwa API Anda mendukung berbagai bahasa dan wilayah dengan benar. Berikut adalah contoh bagaimana Anda dapat menguji i18n dalam aplikasi FastAPI:
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
messages = {
"en": {"greeting": "Hello, world!"},
"fr": {"greeting": "Bonjour le monde !"},
"es": {"greeting": "¡Hola Mundo!"},
}
@app.get("/")
async def read_root(accept_language: Optional[str] = Header(None)):
lang = accept_language[:2] if accept_language else "en"
if lang not in messages:
lang = "en"
return {"message": messages[lang]["greeting"]}
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_read_root_en():
response = client.get("/", headers={"Accept-Language": "en-US"})
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
def test_read_root_fr():
response = client.get("/", headers={"Accept-Language": "fr-FR"})
assert response.status_code == 200
assert response.json() == {"message": "Bonjour le monde !"}
def test_read_root_es():
response = client.get("/", headers={"Accept-Language": "es-ES"})
assert response.status_code == 200
assert response.json() == {"message": "¡Hola Mundo!"}
def test_read_root_default():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}
Contoh ini mengatur header Accept-Language untuk menentukan bahasa yang diinginkan. API mengembalikan sapaan dalam bahasa yang ditentukan. Pengujian memastikan bahwa API menangani preferensi bahasa yang berbeda dengan benar. Jika header Accept-Language tidak ada, bahasa default "en" digunakan.
Kesimpulan
Pengujian adalah bagian penting dari membangun aplikasi FastAPI yang kuat dan andal. TestClient menyediakan cara sederhana dan nyaman untuk menguji titik akhir API Anda. Dengan mengikuti praktik terbaik yang diuraikan dalam panduan ini, Anda dapat menulis pengujian komprehensif yang memastikan kualitas dan stabilitas API Anda. Dari permintaan dasar hingga teknik tingkat lanjut seperti injeksi dependensi dan pengujian asinkron, TestClient memberdayakan Anda untuk membuat kode yang teruji dengan baik dan mudah dipelihara. Rangkullah pengujian sebagai bagian inti dari alur kerja pengembangan Anda, dan Anda akan membangun API yang kuat dan dapat diandalkan bagi pengguna di seluruh dunia. Ingatlah pentingnya integrasi CI/CD untuk mengotomatiskan pengujian dan memastikan jaminan kualitas berkelanjutan.